/*
Copyright (C) 2011 The University of Michigan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Please send inquiries to powertutor@umich.edu
*/
package vn.cybersoft.obs.andriod.batterystats2.components;
import android.content.Context;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.LocationManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;
import vn.cybersoft.obs.andriod.batterystats2.PowerNotifications;
import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.service.IterationData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerData;
import vn.cybersoft.obs.andriod.batterystats2.util.NotificationService;
import vn.cybersoft.obs.andriod.batterystats2.util.Recycler;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;
public class GPS extends PowerComponent {
public static class GpsData extends PowerData {
private static Recycler<GpsData> recycler = new Recycler<GpsData>();
public static GpsData obtain() {
GpsData result = recycler.obtain();
if (result != null)
return result;
return new GpsData();
}
/* The time in seconds since the last iteration of data. */
public double[] stateTimes;
/*
* The number of satellites. This number is only available while the GPS
* is in the on state. Otherwise it is 0.
*/
public int satellites;
private GpsData() {
stateTimes = new double[GPS.POWER_STATES];
}
public void init(double[] stateTimes, int satellites) {
for (int i = 0; i < GPS.POWER_STATES; i++) {
this.stateTimes[i] = stateTimes[i];
}
this.satellites = satellites;
}
@Override
public void recycle() {
recycler.recycle(this);
}
@Override
public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
StringBuilder res = new StringBuilder();
res.append("GPS-state-times");
for (int i = 0; i < GPS.POWER_STATES; i++) {
res.append(" ").append(stateTimes[i]);
}
res.append("\nGPS-sattelites ").append(satellites).append("\n");
out.write(res.toString());
}
}
public static final int POWER_STATES = 3;
public static final int POWER_STATE_OFF = 0;
public static final int POWER_STATE_SLEEP = 1;
public static final int POWER_STATE_ON = 2;
public static final String[] POWER_STATE_NAMES = { "OFF", "SLEEP", "ON" };
private static final String TAG = "GPS";
private static final int HOOK_LIBGPS = 1;
private static final int HOOK_GPS_STATUS_LISTENER = 2;
private static final int HOOK_NOTIFICATIONS = 4;
private static final int HOOK_TIMER = 8;
/* A named pipe written to by the hacked libgps library. */
private static String HOOK_GPS_STATUS_FILE = "/data/misc/gps.status";
private GpsStatus.Listener gpsListener;
private Thread statusThread;
private PowerNotifications notificationReceiver;
private Context context;
private LocationManager locationManager;
private GpsStatus lastStatus;
private boolean hasUidInfo;
private long sleepTime;
private long lastTime;
private GpsStateKeeper gpsState;
private SparseArray<GpsStateKeeper> uidStates;
private static final int GPS_STATUS_SESSION_BEGIN = 1;
private static final int GPS_STATUS_SESSION_END = 2;
private static final int GPS_STATUS_ENGINE_ON = 3;
private static final int GPS_STATUS_ENGINE_OFF = 4;
public GPS(Context context, PhoneConstants constants) {
this.context = context;
uidStates = new SparseArray<GpsStateKeeper>();
sleepTime = (long) Math.round(1000.0 * constants.gpsSleepTime());
hasUidInfo = NotificationService.available();
int hookMethod = 0;
final File gpsStatusFile = new File(HOOK_GPS_STATUS_FILE);
if (gpsStatusFile.exists()) {
/*
* The libgps hack appears to be available. Let's use this to gather
* our status updates from the GPS.
*/
hookMethod = HOOK_LIBGPS;
} else {
/*
* We can always use the status listener hook and perhaps the
* notification hook if we are running eclaire or higher and the
* notification hook is installed. We can only do this on eclaire or
* higher because it wasn't until eclaire that they fixed a bug
* where they didn't maintain a wakelock while the gps engine was
* on.
*/
hookMethod = HOOK_GPS_STATUS_LISTENER;
try {
if (NotificationService.available()
&& Integer.parseInt(Build.VERSION.SDK) >= 5 /*
* eclaire
* or higher
*/) {
hookMethod |= HOOK_NOTIFICATIONS;
}
} catch (NumberFormatException e) {
Log.w(TAG, "Could not parse sdk version: " + Build.VERSION.SDK);
}
}
/*
* If we don't have a way of getting the off<->sleep transitions through
* notifications let's just use a timer and simulat the state of the gps
* instead.
*/
if ((hookMethod & (HOOK_LIBGPS | HOOK_NOTIFICATIONS)) == 0) {
hookMethod |= HOOK_TIMER;
}
/* Create the object that keeps track of the physical GPS state. */
gpsState = new GpsStateKeeper(hookMethod, sleepTime);
/*
* No matter what we are going to register a GpsStatus listener so that
* we can get the satellite count. Also if anything goes wrong with the
* libgps hook we will revert to using this.
*/
locationManager = (LocationManager) context
.getSystemService(Context.LOCATION_SERVICE);
gpsListener = new GpsStatus.Listener() {
public void onGpsStatusChanged(int event) {
if (event == GpsStatus.GPS_EVENT_STARTED) {
gpsState.updateEvent(GPS_STATUS_SESSION_BEGIN,
HOOK_GPS_STATUS_LISTENER);
} else if (event == GpsStatus.GPS_EVENT_STOPPED) {
gpsState.updateEvent(GPS_STATUS_SESSION_END,
HOOK_GPS_STATUS_LISTENER);
}
synchronized (GPS.this) {
lastStatus = locationManager.getGpsStatus(lastStatus);
}
}
};
locationManager.addGpsStatusListener(gpsListener);
/*
* No matter what we register a notification service listener as well so
* that we can get uid information if it's available.
*/
if (hasUidInfo) {
notificationReceiver = new NotificationService.DefaultReceiver() {
public void noteStartWakelock(int uid, String name, int type) {
if (uid == SystemInfo.AID_SYSTEM
&& "GpsLocationProvider".equals(name)) {
gpsState.updateEvent(GPS_STATUS_ENGINE_ON,
HOOK_NOTIFICATIONS);
}
}
public void noteStopWakelock(int uid, String name, int type) {
if (uid == SystemInfo.AID_SYSTEM
&& "GpsLocationProvider".equals(name)) {
gpsState.updateEvent(GPS_STATUS_ENGINE_OFF,
HOOK_NOTIFICATIONS);
}
}
public void noteStartGps(int uid) {
updateUidEvent(uid, GPS_STATUS_SESSION_BEGIN,
HOOK_NOTIFICATIONS);
}
public void noteStopGps(int uid) {
updateUidEvent(uid, GPS_STATUS_SESSION_END,
HOOK_NOTIFICATIONS);
}
};
NotificationService.addHook(notificationReceiver);
}
if (gpsStatusFile.exists()) {
/*
* Start a thread to read from the named pipe and feed us status
* updates.
*/
statusThread = new Thread() {
public void run() {
try {
java.io.FileInputStream fin = new java.io.FileInputStream(
gpsStatusFile);
for (int event = fin.read(); !interrupted()
&& event != -1; event = fin.read()) {
gpsState.updateEvent(event, HOOK_LIBGPS);
}
} catch (IOException e) {
e.printStackTrace();
}
if (!interrupted()) {
// TODO: Have this instead just switch to use different
// hooks.
Log.w(TAG, "GPS status thread exited. "
+ "No longer gathering gps data.");
}
}
};
statusThread.start();
}
}
private void updateUidEvent(int uid, int event, int source) {
synchronized (uidStates) {
GpsStateKeeper state = uidStates.get(uid);
if (state == null) {
state = new GpsStateKeeper(HOOK_NOTIFICATIONS | HOOK_TIMER,
sleepTime, lastTime);
uidStates.put(uid, state);
}
state.updateEvent(event, source);
}
}
@Override
protected void onExit() {
if (gpsListener != null) {
locationManager.removeGpsStatusListener(gpsListener);
}
if (statusThread != null) {
statusThread.interrupt();
}
if (notificationReceiver != null) {
NotificationService.removeHook(notificationReceiver);
}
super.onExit();
}
@Override
public IterationData calculateIteration(long iteration) {
IterationData result = IterationData.obtain();
/* Get the number of satellites that were available in the last update. */
int satellites = 0;
synchronized (this) {
if (lastStatus != null) {
for (GpsSatellite satellite : lastStatus.getSatellites()) {
satellites++;
}
}
}
/* Get the power data for the physical gps device. */
GpsData power = GpsData.obtain();
synchronized (gpsState) {
double[] stateTimes = gpsState.getStateTimesLocked();
int curState = gpsState.getCurrentStateLocked();
power.init(stateTimes, curState == POWER_STATE_ON ? satellites : 0);
gpsState.resetTimesLocked();
}
result.setPowerData(power);
/* Get the power data for each uid if we have information on it. */
if (hasUidInfo)
synchronized (uidStates) {
lastTime = beginTime + iterationInterval * iteration;
for (int i = 0; i < uidStates.size(); i++) {
int uid = uidStates.keyAt(i);
GpsStateKeeper state = uidStates.valueAt(i);
double[] stateTimes = state.getStateTimesLocked();
int curState = state.getCurrentStateLocked();
GpsData uidPower = GpsData.obtain();
uidPower.init(stateTimes,
curState == POWER_STATE_ON ? satellites : 0);
state.resetTimesLocked();
result.addUidPowerData(uid, uidPower);
/*
* Remove state information for uids no longer using the
* gps.
*/
if (curState == POWER_STATE_OFF) {
uidStates.remove(uid);
i--;
}
}
}
return result;
}
@Override
public boolean hasUidInformation() {
return hasUidInfo;
}
/*
* This class is used to maintain the actual GPS state in addition to
* simulating individual uid states.
*/
private static class GpsStateKeeper {
private double[] stateTimes;
private long lastTime;
private int curState;
/* The sum of whatever hook sources are valid. See the HOOK_ constants. */
private int hookMask;
/*
* The time that the GPS hardware should turn off. This is only used if
* HOOK_TIMER is in the hookMask.
*/
private long offTime;
/*
* Gives the time that the GPS stays in the sleep state after the
* session has ended in milliseconds.
*/
private long sleepTime;
public GpsStateKeeper(int hookMask, long sleepTime) {
this(hookMask, sleepTime, SystemClock.elapsedRealtime());
}
public GpsStateKeeper(int hookMask, long sleepTime, long lastTime) {
this.hookMask = hookMask;
this.sleepTime = sleepTime; /*
* This isn't required if HOOK_TIEMR is
* not set.
*/
this.lastTime = lastTime;
stateTimes = new double[POWER_STATES];
curState = POWER_STATE_OFF;
offTime = -1;
}
/* Make sure that you have a lock on this before calling. */
public double[] getStateTimesLocked() {
updateTimesLocked();
/*
* Let's normalize the times so that power measurements are
* consistent.
*/
double total = 0;
for (int i = 0; i < POWER_STATES; i++) {
total += stateTimes[i];
}
if (total == 0)
total = 1;
for (int i = 0; i < POWER_STATES; i++) {
stateTimes[i] /= total;
}
return stateTimes;
}
public void resetTimesLocked() {
for (int i = 0; i < POWER_STATES; i++) {
stateTimes[i] = 0;
}
}
public int getCurrentStateLocked() {
return curState;
}
/* Make sure that you have a lock on this before calling. */
private void updateTimesLocked() {
/* Update the time we were in the previous state. */
long curTime = SystemClock.elapsedRealtime();
/* Check if the GPS has gone to sleep as a result of a timer. */
if ((hookMask & HOOK_TIMER) != 0 && offTime != -1
&& offTime < curTime) {
stateTimes[curState] += (offTime - lastTime) / 1000.0;
curState = POWER_STATE_OFF;
offTime = -1;
}
/* Update the amount of time that we've been in the current state. */
stateTimes[curState] += (curTime - lastTime) / 1000.0;
lastTime = curTime;
}
/*
* When a hook source gets an event it should report it to updateEvent.
* The only exception is HOOK_TIMER which is handled within this class
* itself.
*/
public void updateEvent(int event, int source) {
synchronized (this) {
if ((hookMask & source) == 0) {
/* We are not using this hook source, ignore. */
return;
}
updateTimesLocked();
int oldState = curState;
switch (event) {
case GPS_STATUS_SESSION_BEGIN:
curState = POWER_STATE_ON;
break;
case GPS_STATUS_SESSION_END:
if (curState == POWER_STATE_ON) {
curState = POWER_STATE_SLEEP;
}
break;
case GPS_STATUS_ENGINE_ON:
if (curState == POWER_STATE_OFF) {
curState = POWER_STATE_SLEEP;
}
break;
case GPS_STATUS_ENGINE_OFF:
curState = POWER_STATE_OFF;
break;
default:
Log.w(TAG, "Unknown GPS event captured");
}
if (curState != oldState) {
if (oldState == POWER_STATE_ON
&& curState == POWER_STATE_SLEEP) {
offTime = SystemClock.elapsedRealtime() + sleepTime;
} else {
/*
* Any other state transition should reset the off
* timer.
*/
offTime = -1;
}
}
}
}
}
@Override
public String getComponentName() {
return "GPS";
}
}